package playback

import (
	"io"
	"os"
	"testing"
	"time"

	amp4 "github.com/abema/go-mp4"
	"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio"
	"github.com/bluenviron/mediacommon/v2/pkg/formats/fmp4"
	"github.com/bluenviron/mediacommon/v2/pkg/formats/mp4"
	"github.com/bluenviron/mediamtx/internal/recordstore"
	"github.com/bluenviron/mediamtx/internal/test"
	"github.com/stretchr/testify/require"
)

func writeBenchInit(f io.WriteSeeker) {
	init := fmp4.Init{
		Tracks: []*fmp4.InitTrack{
			{
				ID:        1,
				TimeScale: 90000,
				Codec: &mp4.CodecH264{
					SPS: test.FormatH264.SPS,
					PPS: test.FormatH264.PPS,
				},
			},
			{
				ID:        2,
				TimeScale: 90000,
				Codec: &mp4.CodecMPEG4Audio{
					Config: mpeg4audio.AudioSpecificConfig{
						Type:         mpeg4audio.ObjectTypeAACLC,
						SampleRate:   48000,
						ChannelCount: 2,
					},
				},
			},
		},
	}

	err := init.Marshal(f)
	if err != nil {
		panic(err)
	}

	_, err = f.Write([]byte{
		0x00, 0x00, 0x00, 0x10, 'm', 'o', 'o', 'f',
	})
	if err != nil {
		panic(err)
	}
}

func BenchmarkFMP4ReadHeader(b *testing.B) {
	f, err := os.CreateTemp(os.TempDir(), "mediamtx-playback-fmp4-")
	if err != nil {
		panic(err)
	}
	defer os.Remove(f.Name())

	writeBenchInit(f)
	f.Close()

	for b.Loop() {
		func() {
			f, err = os.Open(f.Name())
			if err != nil {
				panic(err)
			}
			defer f.Close()

			_, _, err = segmentFMP4ReadHeader(f)
			if err != nil {
				panic(err)
			}
		}()
	}
}

func TestSegmentFMP4CanBeConcatenated(t *testing.T) {
	baseTime := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
	streamID1 := [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}
	streamID2 := [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 17}

	baseTracks := []*fmp4.InitTrack{
		{
			ID:        1,
			TimeScale: 90000,
			Codec: &mp4.CodecH264{
				SPS: test.FormatH264.SPS,
				PPS: test.FormatH264.PPS,
			},
		},
		{
			ID:        2,
			TimeScale: 48000,
			Codec: &mp4.CodecMPEG4Audio{
				Config: mpeg4audio.AudioSpecificConfig{
					Type:         mpeg4audio.ObjectTypeAACLC,
					SampleRate:   48000,
					ChannelCount: 2,
				},
			},
		},
	}

	differentTracks := []*fmp4.InitTrack{
		{
			ID:        1,
			TimeScale: 90000,
			Codec: &mp4.CodecH264{
				SPS: test.FormatH264.SPS,
				PPS: test.FormatH264.PPS,
			},
		},
	}

	for _, tt := range []struct {
		name     string
		prevInit *fmp4.Init
		prevEnd  time.Time
		curInit  *fmp4.Init
		curStart time.Time
		want     bool
	}{
		{
			name: "with mtxi - consecutive segments, same stream",
			prevInit: &fmp4.Init{
				Tracks: baseTracks,
				UserData: []amp4.IBox{
					&recordstore.Mtxi{
						StreamID:      streamID1,
						SegmentNumber: 1,
					},
				},
			},
			prevEnd: baseTime,
			curInit: &fmp4.Init{
				Tracks: baseTracks,
				UserData: []amp4.IBox{
					&recordstore.Mtxi{
						StreamID:      streamID1,
						SegmentNumber: 2,
					},
				},
			},
			curStart: baseTime.Add(5 * time.Second),
			want:     true,
		},
		{
			name: "with mtxi - non-consecutive segments, same stream",
			prevInit: &fmp4.Init{
				Tracks: baseTracks,
				UserData: []amp4.IBox{
					&recordstore.Mtxi{
						StreamID:      streamID1,
						SegmentNumber: 1,
					},
				},
			},
			prevEnd: baseTime,
			curInit: &fmp4.Init{
				Tracks: baseTracks,
				UserData: []amp4.IBox{
					&recordstore.Mtxi{
						StreamID:      streamID1,
						SegmentNumber: 3,
					},
				},
			},
			curStart: baseTime.Add(5 * time.Second),
			want:     false,
		},
		{
			name: "with mtxi - consecutive segments, different streams",
			prevInit: &fmp4.Init{
				Tracks: baseTracks,
				UserData: []amp4.IBox{
					&recordstore.Mtxi{
						StreamID:      streamID1,
						SegmentNumber: 1,
					},
				},
			},
			prevEnd: baseTime,
			curInit: &fmp4.Init{
				Tracks: baseTracks,
				UserData: []amp4.IBox{
					&recordstore.Mtxi{
						StreamID:      streamID2,
						SegmentNumber: 2,
					},
				},
			},
			curStart: baseTime.Add(5 * time.Second),
			want:     false,
		},
		{
			name: "prev has mtxi, current does not",
			prevInit: &fmp4.Init{
				Tracks: baseTracks,
				UserData: []amp4.IBox{
					&recordstore.Mtxi{
						StreamID:      streamID1,
						SegmentNumber: 1,
					},
				},
			},
			prevEnd: baseTime,
			curInit: &fmp4.Init{
				Tracks:   baseTracks,
				UserData: []amp4.IBox{},
			},
			curStart: baseTime.Add(5 * time.Second),
			want:     false,
		},
		{
			name: "prev does not have mtxi, current has mtxi",
			prevInit: &fmp4.Init{
				Tracks:   baseTracks,
				UserData: []amp4.IBox{},
			},
			prevEnd: baseTime,
			curInit: &fmp4.Init{
				Tracks: baseTracks,
				UserData: []amp4.IBox{
					&recordstore.Mtxi{
						StreamID:      streamID1,
						SegmentNumber: 1,
					},
				},
			},
			curStart: baseTime.Add(5 * time.Second),
			want:     false,
		},
		{
			name: "legacy mode - same tracks, within time tolerance",
			prevInit: &fmp4.Init{
				Tracks:   baseTracks,
				UserData: []amp4.IBox{},
			},
			prevEnd: baseTime.Add(10 * time.Second),
			curInit: &fmp4.Init{
				Tracks:   baseTracks,
				UserData: []amp4.IBox{},
			},
			curStart: baseTime.Add(10 * time.Second),
			want:     true,
		},
		{
			name: "legacy mode - same tracks, exactly at tolerance boundary (before)",
			prevInit: &fmp4.Init{
				Tracks:   baseTracks,
				UserData: []amp4.IBox{},
			},
			prevEnd: baseTime.Add(10 * time.Second),
			curInit: &fmp4.Init{
				Tracks:   baseTracks,
				UserData: []amp4.IBox{},
			},
			curStart: baseTime.Add(9 * time.Second),
			want:     true,
		},
		{
			name: "legacy mode - same tracks, exactly at tolerance boundary (after)",
			prevInit: &fmp4.Init{
				Tracks:   baseTracks,
				UserData: []amp4.IBox{},
			},
			prevEnd: baseTime.Add(10 * time.Second),
			curInit: &fmp4.Init{
				Tracks:   baseTracks,
				UserData: []amp4.IBox{},
			},
			curStart: baseTime.Add(11 * time.Second),
			want:     true,
		},
		{
			name: "legacy mode - same tracks, outside time tolerance (too early)",
			prevInit: &fmp4.Init{
				Tracks:   baseTracks,
				UserData: []amp4.IBox{},
			},
			prevEnd: baseTime.Add(10 * time.Second),
			curInit: &fmp4.Init{
				Tracks:   baseTracks,
				UserData: []amp4.IBox{},
			},
			curStart: baseTime.Add(8*time.Second + 999*time.Millisecond),
			want:     false,
		},
		{
			name: "legacy mode - same tracks, outside time tolerance (too late)",
			prevInit: &fmp4.Init{
				Tracks:   baseTracks,
				UserData: []amp4.IBox{},
			},
			prevEnd: baseTime.Add(10 * time.Second),
			curInit: &fmp4.Init{
				Tracks:   baseTracks,
				UserData: []amp4.IBox{},
			},
			curStart: baseTime.Add(11*time.Second + 1*time.Millisecond),
			want:     false,
		},
		{
			name: "legacy mode - different number of tracks",
			prevInit: &fmp4.Init{
				Tracks:   baseTracks,
				UserData: []amp4.IBox{},
			},
			prevEnd: baseTime,
			curInit: &fmp4.Init{
				Tracks:   differentTracks,
				UserData: []amp4.IBox{},
			},
			curStart: baseTime.Add(5 * time.Second),
			want:     false,
		},
		{
			name: "legacy mode - different track IDs",
			prevInit: &fmp4.Init{
				Tracks: []*fmp4.InitTrack{
					{
						ID:        1,
						TimeScale: 90000,
						Codec: &mp4.CodecH264{
							SPS: test.FormatH264.SPS,
							PPS: test.FormatH264.PPS,
						},
					},
				},
				UserData: []amp4.IBox{},
			},
			prevEnd: baseTime,
			curInit: &fmp4.Init{
				Tracks: []*fmp4.InitTrack{
					{
						ID:        2,
						TimeScale: 90000,
						Codec: &mp4.CodecH264{
							SPS: test.FormatH264.SPS,
							PPS: test.FormatH264.PPS,
						},
					},
				},
				UserData: []amp4.IBox{},
			},
			curStart: baseTime.Add(5 * time.Second),
			want:     false,
		},
		{
			name: "legacy mode - different time scales",
			prevInit: &fmp4.Init{
				Tracks: []*fmp4.InitTrack{
					{
						ID:        1,
						TimeScale: 90000,
						Codec: &mp4.CodecH264{
							SPS: test.FormatH264.SPS,
							PPS: test.FormatH264.PPS,
						},
					},
				},
				UserData: []amp4.IBox{},
			},
			prevEnd: baseTime,
			curInit: &fmp4.Init{
				Tracks: []*fmp4.InitTrack{
					{
						ID:        1,
						TimeScale: 48000,
						Codec: &mp4.CodecH264{
							SPS: test.FormatH264.SPS,
							PPS: test.FormatH264.PPS,
						},
					},
				},
				UserData: []amp4.IBox{},
			},
			curStart: baseTime.Add(5 * time.Second),
			want:     false,
		},
		{
			name: "legacy mode - different codec types",
			prevInit: &fmp4.Init{
				Tracks: []*fmp4.InitTrack{
					{
						ID:        1,
						TimeScale: 90000,
						Codec: &mp4.CodecH264{
							SPS: test.FormatH264.SPS,
							PPS: test.FormatH264.PPS,
						},
					},
				},
				UserData: []amp4.IBox{},
			},
			prevEnd: baseTime,
			curInit: &fmp4.Init{
				Tracks: []*fmp4.InitTrack{
					{
						ID:        1,
						TimeScale: 90000,
						Codec: &mp4.CodecMPEG4Audio{
							Config: mpeg4audio.AudioSpecificConfig{
								Type:         mpeg4audio.ObjectTypeAACLC,
								SampleRate:   48000,
								ChannelCount: 2,
							},
						},
					},
				},
				UserData: []amp4.IBox{},
			},
			curStart: baseTime.Add(5 * time.Second),
			want:     false,
		},
	} {
		t.Run(tt.name, func(t *testing.T) {
			got := segmentFMP4CanBeConcatenated(tt.prevInit, tt.prevEnd, tt.curInit, tt.curStart)
			require.Equal(t, tt.want, got)
		})
	}
}
